通过SpringBoot构建你的MCP Server
1. 背景
在传统的人机交互中,AI 获取信息的来源主要依赖于预先训练时所投喂的大规模语料库,或是在有限权限下的联网支持。因此,如果你希望 AI 分析你自己的网站内容, 或者结合你在知乎上的所有回答进行深入分析,传统 AI 是无法完成的。
而 MCP Server 正好解决了这个问题,它的本质是为 AI 接入更多信息源,相当于为 AI 扩展了「感知范围」。通过 MCP Server,你可以让 AI 实时访问你提供的数据,比如网页、数据库、文档接口等,从而进行更具上下文的分析与响应。
当然,这种能力的代价是:你需要自己整合并维护这些希望让 AI 访问的数据源。
那么怎么做?下面的内容会用Java SpringBoot 实现一个最简单的案例:搭建一个MCP 天气服务器,让Claude Desktop能够知道实时的天气。
2. 通过Spring AI构建Spring AI MCP Server
对于Java开发者来说,SpringBoot是再熟悉不过的框架,所以这里我们直接下载已经构建的Spring AI MCP Weather STDIO Server项目进行讲解。
代码位置:starter-stdio-server, 另外你可以找到更多的案例在代码仓库:spring-ai-examples
2.1 项目解释:pom.xml
从pom.xml中我们能很看出,是直接使用的spring-ai-starter-mcp-server,spring-web两个依赖,没有其他的附加依赖。
- spring-ai-starter-mcp-server 是 Spring AI 官方提供的一个 用于构建自定义 MCP Server 的依赖。
- 而spring-web 则更熟悉的,是 Spring Framework 中的一个核心模块,它提供了构建 Web 应用程序所需的基础功能。
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
</dependencies>
2.2 项目解释:application.properties
spring.main.web-application-type=none
# 注意:您必须禁用横幅和控制台日志记录以便让 STDIO 传输系统正常工作!!!
spring.main.banner-mode=off
logging.pattern.console=
spring.ai.mcp.server.name=my-weather-server
spring.ai.mcp.server.version=0.0.1
logging.file.name=./model-context-protocol/weather/starter-stdio-server/target/mcp-weather-stdio-server.log
spring.main.web-application-type=none:关闭 Spring Boot 的 Web 功能(不启动 Servlet 或 WebFlux 容器)。spring.main.banner-mode=off:关闭 Spring Boot 启动时的 ASCII 艺术字横幅,因为横幅会写到 stdout,可能干扰 STDIO 通信协议(AI 工具依赖 stdout 的 JSON 格式)。logging.pattern.console=: 清空控制台日志格式,防止出现前缀、时间戳、级别等多余内容。确保标准输出中只输出结构化 JSON 响应或插件数据,不被干扰。logging.file.name=./model-context-protocol/weather/starter-stdio-server/target/mcp-weather-stdio-server.log:将日志输出写入指定的文件路径。避免将日志打印到控制台(stdout),干扰 MCP 的 stdio 通信;同时方便后期排查问题。
2.3 具体实现
从项目结构中可以看到,整个项目只有 McpServerApplication.java 和 WeatherService.java 两个类
├── README.md
├── mvnw
├── mvnw.cmd
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── org
│ │ │ └── springframework
│ │ │ └── ai
│ │ │ └── mcp
│ │ │ └── sample
│ │ │ └── server
│ │ │ ├── McpServerApplication.java
│ │ │ └── WeatherService.java
│ │ └── resources
│ │ └── application.properties
32 directories, 21 files
McpServerApplication.java 很简单,就是项目启动类,然后当中注入了一个bean。
而注入的这个 bean ToolCallbackProvider 是 Spring AI 中用于 管理和调度 AI 调用工具(Tool)时的回调处理逻辑 的接口。
Tool 是你注册给 AI 模型可以调用的函数(类似于 OpenAI 的 Function Calling 或 Tools 功能)。当 AI 触发某个 Tool 的调用时,
ToolCallbackProvider 就负责:查找并调用你注册的具体处理逻辑(回调函数)。
.toolObjects(weatherService) 则是把另一个bean WeatherService 给加进去,就意味着把 weatherService 以 tool 的形式提供给AI
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
@Bean
public ToolCallbackProvider weatherTools(WeatherService weatherService) {
return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
}
}
那我们来看真正发挥作用的 WeatherService.java, 这里我调整了一下代码位置方便我们一眼就能注意到有两个方法被 @Tool注解修饰。
这个注解的主要作用就是 在Spring AI中将一个方法标记为工具。
而方法入参数被注解 @ToolParam 修饰则是给方法参数添加描述信息,便于 AI 理解参数含义
而这个Service做的作用就是,当AI调用的时候,请求 api.weather.gov 接口获取天气信息,再输出给用户
• 使用 RestClient 调用 api.weather.gov 获取天气数据
• 暴露了两个 AI 可调用的工具(方法),可接入 GPT/Claude 等支持工具调用的模型
• 结合 @Tool和 MCP 协议,可以通过 ChatGPT 插件或 Spring AI Assistant 无缝接入这些后端服务
提供两个 AI 可调用的接口:
getWeatherForecastByLocation(double latitude, double longitude)— 获取某地天气预报getAlerts(String state)— 获取某个美国州的天气警报
@Service
public class WeatherService {
private static final String BASE_URL = "https://api.weather.gov";
private final RestClient restClient;
public WeatherService() {
this.restClient = RestClient.builder()
.baseUrl(BASE_URL)
.defaultHeader("Accept", "application/geo+json")
.defaultHeader("User-Agent", "WeatherApiClient/1.0 (your@email.com)")
.build();
}
/**
* 获取特定纬度/经度的天气预报
* @param latitude Latitude
* @param longitude Longitude
* @return The forecast for the given location
* @throws RestClientException if the request fails
*/
@Tool(description = "Get weather forecast for a specific latitude/longitude")
public String getWeatherForecastByLocation(double latitude, double longitude) {
var points = restClient.get()
.uri("/points/{latitude},{longitude}", latitude, longitude)
.retrieve()
.body(Points.class);
var forecast = restClient.get().uri(points.properties().forecast()).retrieve().body(Forecast.class);
String forecastText = forecast.properties().periods().stream().map(p -> {
return String.format("""
%s:
Temperature: %s %s
Wind: %s %s
Forecast: %s
""", p.name(), p.temperature(), p.temperatureUnit(), p.windSpeed(), p.windDirection(),
p.detailedForecast());
}).collect(Collectors.joining());
return forecastText;
}
/**
* 获取特定区域的警报
* @param 州区号。两个字母的美国州代码(如CA, NY)
* @return 人类可读的警报信息
* @throws RestClientException if the request fails
*/
@Tool(description = "Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY)")
public String getAlerts(@ToolParam( description = "Two-letter US state code (e.g. CA, NY") String state) {
Alert alert = restClient.get().uri("/alerts/active/area/{state}", state).retrieve().body(Alert.class);
return alert.features()
.stream()
.map(f -> String.format("""
Event: %s
Area: %s
Severity: %s
Description: %s
Instructions: %s
""", f.properties().event(), f.properties.areaDesc(), f.properties.severity(),
f.properties.description(), f.properties.instruction()))
.collect(Collectors.joining("\n"));
}
@JsonIgnoreProperties(ignoreUnknown = true)
public record Points(@JsonProperty("properties") Props properties) {
@JsonIgnoreProperties(ignoreUnknown = true)
public record Props(@JsonProperty("forecast") String forecast) {
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public record Forecast(@JsonProperty("properties") Props properties) {
@JsonIgnoreProperties(ignoreUnknown = true)
public record Props(@JsonProperty("periods") List<Period> periods) {
}
@JsonIgnoreProperties(ignoreUnknown = true)
public record Period(@JsonProperty("number") Integer number, @JsonProperty("name") String name,
@JsonProperty("startTime") String startTime, @JsonProperty("endTime") String endTime,
@JsonProperty("isDaytime") Boolean isDayTime, @JsonProperty("temperature") Integer temperature,
@JsonProperty("temperatureUnit") String temperatureUnit,
@JsonProperty("temperatureTrend") String temperatureTrend,
@JsonProperty("probabilityOfPrecipitation") Map probabilityOfPrecipitation,
@JsonProperty("windSpeed") String windSpeed, @JsonProperty("windDirection") String windDirection,
@JsonProperty("icon") String icon, @JsonProperty("shortForecast") String shortForecast,
@JsonProperty("detailedForecast") String detailedForecast) {
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
public record Alert(@JsonProperty("features") List<Feature> features) {
@JsonIgnoreProperties(ignoreUnknown = true)
public record Feature(@JsonProperty("properties") Properties properties) {
}
@JsonIgnoreProperties(ignoreUnknown = true)
public record Properties(@JsonProperty("event") String event, @JsonProperty("areaDesc") String areaDesc,
@JsonProperty("severity") String severity, @JsonProperty("description") String description,
@JsonProperty("instruction") String instruction) {
}
}
}
2.3 使用项目
当你想要启动这个项目的时候,需要Java 17以上版本,Maven 3.6以上以上版本,此时你肯定很顺滑直接启动了。
但是实际上使用的方式需要先打成一个Jar包,在项目根目录执行:mvn package, 就能在target目录下看到 mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar

接着需要你本地已经安装了Claude for Desktop, 没有的话可以在这里下载:https://claude.ai/download
安装后,使用文本编辑器打开:
- Mac
- Windows
~/Library/Application Support/Claude/claude_desktop_config.json
AppData\Claude\claude_desktop_config.json
没有的话就对这个文件进行创建。
接着写入下面的内容,记得修改 /ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar 为你打出jar包的绝对路径
{
"mcpServers": {
"spring-ai-mcp-weather": {
"command": "java",
"args": [
"-Dspring.ai.mcp.server.stdio=true",
"-jar",
"/ABSOLUTE/PATH/TO/PARENT/FOLDER/mcp-weather-stdio-server-0.0.1-SNAPSHOT.jar"
]
}
}
}
最后一步,重启你的 Claude,然后你甚至能直接问AI: 你接入了几个MCP server
接着AI会告诉你的。

然后你再问他,美国洛杉矶天气如何,就会实际调用你的这个jar包的

到此大功告成!快去扩展你的MCP Server吧。